#include <efsw/WatcherKqueue.hpp>

#if EFSW_PLATFORM == EFSW_PLATFORM_KQUEUE || EFSW_PLATFORM == EFSW_PLATFORM_FSEVENTS

#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <efsw/Debug.hpp>
#include <efsw/String.hpp>
#include <efsw/System.hpp>
#include <efsw/FileSystem.hpp>
#include <efsw/WatcherGeneric.hpp>
#include <efsw/FileWatcherKqueue.hpp>

#define KEVENT_RESERVE_VALUE (10)

#ifndef O_EVTONLY
#define O_EVTONLY (O_RDONLY | O_NONBLOCK)
#endif

namespace efsw {

int comparator(const void* ke1, const void* ke2)
{
    const KEvent * kev1    = reinterpret_cast<const KEvent*>( ke1 );
    const KEvent * kev2    = reinterpret_cast<const KEvent*>( ke2 );

    if ( NULL != kev2->udata )
    {
        FileInfo * fi1        = reinterpret_cast<FileInfo*>( kev1->udata );
        FileInfo * fi2        = reinterpret_cast<FileInfo*>( kev2->udata );

        return strcmp( fi1->Filepath.c_str(), fi2->Filepath.c_str() );
    }

    return 1;
}

WatcherKqueue::WatcherKqueue(WatchID watchid, const std::string& dirname, FileWatchListener* listener, bool recursive, FileWatcherKqueue * watcher, WatcherKqueue * parent ) :
    Watcher( watchid, dirname, listener, recursive ),
    mLastWatchID(0),
    mChangeListCount( 0 ),
    mKqueue( kqueue() ),
    mWatcher( watcher ),
    mParent( parent ),
    mInitOK( true ),
    mErrno(0)
{
    if ( -1 == mKqueue )
    {
        efDEBUG( "kqueue() returned invalid descriptor for directory %s. File descriptors count: %ld\n", Directory.c_str(), mWatcher->mFileDescriptorCount );
        
        mInitOK = false;
        mErrno = errno;
    }
    else
    {
        mWatcher->addFD();
    }
}

WatcherKqueue::~WatcherKqueue()
{
    // Remove the childs watchers ( sub-folders watches )
    removeAll();

    for ( size_t i = 0; i < mChangeListCount; i++ )
    {
        if ( NULL != mChangeList[i].udata )
        {
            FileInfo * fi        = reinterpret_cast<FileInfo*>( mChangeList[i].udata );

            efSAFE_DELETE( fi );
        }
    }

    close( mKqueue );
    
    mWatcher->removeFD();
}

void WatcherKqueue::addAll()
{
    if ( -1 == mKqueue )
    {
        return;
    }

    // scan directory and call addFile(name, false) on each file
    FileSystem::dirAddSlashAtEnd( Directory );

    efDEBUG( "addAll(): Added folder: %s\n", Directory.c_str());

    // add base dir
    int fd = open( Directory.c_str(), O_EVTONLY );
    
    if ( -1 == fd )
    {
        efDEBUG( "addAll(): Couldn't open folder: %s\n", Directory.c_str() );
        
        if ( EACCES != errno )
        {
            mInitOK = false;
        }

        mErrno = errno;
        
        return;
    }

    mDirSnap.setDirectoryInfo( Directory );
    mDirSnap.scan();

    mChangeList.resize( KEVENT_RESERVE_VALUE );

    // Creates the kevent for the folder
    EV_SET(
        &mChangeList[0],
        fd,
        EVFILT_VNODE,
        EV_ADD | EV_ENABLE | EV_ONESHOT,
        NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME,
        0,
        0
    );

    mWatcher->addFD();

    // Get the files and directories from the directory
    FileInfoMap files = FileSystem::filesInfoFromPath( Directory );

    for ( FileInfoMap::iterator it = files.begin(); it != files.end(); it++ )
    {
        FileInfo& fi = it->second;

        if ( fi.isRegularFile() )
        {
            // Add the regular files kevent
            addFile( fi.Filepath , false );
        }
        else if ( Recursive && fi.isDirectory() && fi.isReadable() )
        {
            // Create another watcher for the subfolders ( if recursive )
            WatchID id = addWatch( fi.Filepath, Listener, Recursive, this );

            // If the watcher is not adding the watcher means that the directory was created
            if ( id > 0 && !mWatcher->isAddingWatcher() )
            {
                handleFolderAction( fi.Filepath, Actions::Add );
            }
        }
    }
}

void WatcherKqueue::removeAll()
{
    efDEBUG( "removeAll(): Removing all child watchers\n" );

    std::list<WatchID> erase;

    for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); it++ )
    {
        efDEBUG( "removeAll(): Removed child watcher %s\n", it->second->Directory.c_str() );

        erase.push_back( it->second->ID );
    }

    for ( std::list<WatchID>::iterator eit = erase.begin(); eit != erase.end(); eit++ )
    {
        removeWatch( *eit );
    }
}

void WatcherKqueue::addFile(const std::string& name, bool emitEvents)
{
    efDEBUG( "addFile(): Added: %s\n", name.c_str() );

    // Open the file to get the file descriptor
    int fd = open( name.c_str(), O_EVTONLY );

    if( fd == -1 )
    {
        efDEBUG( "addFile(): Could open file descriptor for %s. File descriptor count: %ld\n", name.c_str(), mWatcher->mFileDescriptorCount );
        
        Errors::Log::createLastError( Errors::FileNotReadable, name );
        
        if ( EACCES != errno )
        {
            mInitOK = false;
        }

        mErrno = errno;
        
        return;
    }

    mWatcher->addFD();

    // increase the file kevent file count
    mChangeListCount++;

    if ( mChangeListCount + KEVENT_RESERVE_VALUE > mChangeList.size() &&
         mChangeListCount % KEVENT_RESERVE_VALUE == 0 )
    {
        size_t reserve_size = mChangeList.size() + KEVENT_RESERVE_VALUE;
        mChangeList.resize( reserve_size );
        efDEBUG( "addFile(): Reserverd more KEvents space for %s, space reserved %ld, list actual size %ld.\n", Directory.c_str(), reserve_size, mChangeListCount );
    }

    // create entry
    FileInfo * entry = new FileInfo( name );

    // set the event data at the end of the list
    EV_SET(
        &mChangeList[mChangeListCount],
        fd,
        EVFILT_VNODE,
        EV_ADD | EV_ENABLE | EV_ONESHOT,
        NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME,
        0,
        (void*)entry
    );

    // qsort sort the list by name
    qsort(&mChangeList[1], mChangeListCount, sizeof(KEvent), comparator);

    // handle action
    if( emitEvents )
    {
        handleAction(name, Actions::Add);
    }
}

void WatcherKqueue::removeFile( const std::string& name, bool emitEvents )
{
    efDEBUG( "removeFile(): Trying to remove file: %s\n", name.c_str() );

    // bsearch
    KEvent target;

    // Create a temporary file info to search the kevent ( searching the directory )
    FileInfo tempEntry( name );

    target.udata = &tempEntry;

    // Search the kevent
    KEvent * ke = (KEvent*)bsearch(&target, &mChangeList[0], mChangeListCount + 1, sizeof(KEvent), comparator);

    // Trying to remove a non-existing file?
    if( !ke )
    {
        Errors::Log::createLastError( Errors::FileNotFound, name );
        efDEBUG( "File not removed\n" );
        return;
    }

    efDEBUG( "File removed\n" );

    // handle action
    if ( emitEvents )
    {
        handleAction( name, Actions::Delete );
    }

    // Delete the user data ( FileInfo ) from the kevent closed
    FileInfo * del = reinterpret_cast<FileInfo*>( ke->udata );

    efSAFE_DELETE( del );

    // close the file descriptor from the kevent
    close( ke->ident );

    mWatcher->removeFD();

    memset(ke, 0, sizeof(KEvent));

    // move end to current
    memcpy(ke, &mChangeList[mChangeListCount], sizeof(KEvent));
    memset(&mChangeList[mChangeListCount], 0, sizeof(KEvent));
    --mChangeListCount;
}

void WatcherKqueue::rescan()
{
    efDEBUG( "rescan(): Rescanning: %s\n", Directory.c_str() );

    DirectorySnapshotDiff Diff = mDirSnap.scan();

    if ( Diff.DirChanged )
    {
        sendDirChanged();
    }

    if ( Diff.changed() )
    {
        FileInfoList::iterator it;
        MovedList::iterator mit;

        /// Files
        DiffIterator( FilesCreated )
        {
            addFile( (*it).Filepath );
        }

        DiffIterator( FilesModified )
        {
            handleAction( (*it).Filepath, Actions::Modified );
        }

        DiffIterator( FilesDeleted )
        {
            removeFile( (*it).Filepath );
        }

        DiffMovedIterator( FilesMoved )
        {
            handleAction( (*mit).second.Filepath, Actions::Moved, (*mit).first );
            removeFile( Directory + (*mit).first, false );
            addFile( (*mit).second.Filepath, false );
        }

        /// Directories
        DiffIterator( DirsCreated )
        {
            handleFolderAction( (*it).Filepath, Actions::Add );
            addWatch( (*it).Filepath, Listener, Recursive, this );
        }

        DiffIterator( DirsModified )
        {
            handleFolderAction( (*it).Filepath, Actions::Modified );
        }

        DiffIterator( DirsDeleted )
        {
            handleFolderAction( (*it).Filepath, Actions::Delete );

            Watcher * watch = findWatcher( (*it).Filepath );

            if ( NULL != watch )
            {
                removeWatch( watch->ID );

            }
        }

        DiffMovedIterator( DirsMoved )
        {
            moveDirectory( Directory + (*mit).first, (*mit).second.Filepath );
        }
    }
}

WatchID WatcherKqueue::watchingDirectory( std::string dir )
{
    Watcher * watch = findWatcher( dir );

    if ( NULL != watch )
    {
        return watch->ID;
    }

    return Errors::FileNotFound;
}

void WatcherKqueue::handleAction( const std::string& filename, efsw::Action action, const std::string& oldFilename )
{
    Listener->handleFileAction( ID, Directory, FileSystem::fileNameFromPath( filename ), action, FileSystem::fileNameFromPath( oldFilename ) );
}

void WatcherKqueue::handleFolderAction( std::string filename, efsw::Action action , const std::string &oldFilename )
{
    FileSystem::dirRemoveSlashAtEnd( filename );

    handleAction( filename, action, oldFilename );
}

void WatcherKqueue::sendDirChanged()
{
    if ( NULL != mParent )
    {
        Listener->handleFileAction( mParent->ID, mParent->Directory, FileSystem::fileNameFromPath( Directory ), Actions::Modified );
    }
}

void WatcherKqueue::watch()
{
    if ( -1 == mKqueue )
    {
        return;
    }

    int nev = 0;
    KEvent event;

    // First iterate the childs, to get the events from the deepest folder, to the watcher childs
    for ( WatchMap::iterator it = mWatches.begin(); it != mWatches.end(); ++it )
    {
        it->second->watch();
    }

    bool needScan = false;

    // Then we get the the events of the current folder
    while( ( nev = kevent( mKqueue, &mChangeList[0], mChangeListCount + 1, &event, 1, &mWatcher->mTimeOut ) ) != 0 )
    {
        // An error ocurred?
        if( nev == -1 )
        {
            efDEBUG( "watch(): Error on directory %s\n", Directory.c_str() );
            perror("kevent");
            break;
        }
        else
        {
            FileInfo * entry = NULL;

            // If udate == NULL means that it is the fisrt element of the change list, the folder.
            // otherwise it is an event of some file inside the folder
            if( ( entry = reinterpret_cast<FileInfo*> ( event.udata ) ) != NULL )
            {
                efDEBUG( "watch(): File: %s ", entry->Filepath.c_str() );

                // If the event flag is delete... the file was deleted
                if ( event.fflags & NOTE_DELETE )
                {
                    efDEBUG( "deleted\n" );

                    mDirSnap.removeFile( entry->Filepath );

                    removeFile( entry->Filepath );
                }
                else if (    event.fflags & NOTE_EXTEND    ||
                            event.fflags & NOTE_WRITE    ||
                            event.fflags & NOTE_ATTRIB
                )
                {
                    // The file was modified
                    efDEBUG( "modified\n" );

                    FileInfo fi( entry->Filepath );

                    if ( fi != *entry )
                    {
                        *entry = fi;

                        mDirSnap.updateFile( entry->Filepath );

                        handleAction( entry->Filepath, efsw::Actions::Modified );
                    }
                }
                else if ( event.fflags & NOTE_RENAME )
                {
                    efDEBUG( "moved\n" );

                    needScan = true;
                }
            }
            else
            {
                needScan = true;
            }
        }
    }

    if ( needScan )
    {
        rescan();
    }
}

Watcher * WatcherKqueue::findWatcher( const std::string path )
{
    WatchMap::iterator it = mWatches.begin();

    for ( ; it != mWatches.end(); it++ )
    {
        if ( it->second->Directory == path )
        {
            return it->second;
        }
    }

    return NULL;
}

void WatcherKqueue::moveDirectory( std::string oldPath, std::string newPath, bool emitEvents )
{
    // Update the directory path if it's a watcher
    std::string opath2( oldPath );
    FileSystem::dirAddSlashAtEnd( opath2 );

    Watcher * watch = findWatcher( opath2 );

    if ( NULL != watch )
    {
        watch->Directory = opath2;
    }

    if ( emitEvents )
    {
        handleFolderAction( newPath, efsw::Actions::Moved, oldPath );
    }
}

WatchID WatcherKqueue::addWatch( const std::string& directory, FileWatchListener* watcher, bool recursive , WatcherKqueue *parent)
{
    static long s_fc = 0;
    static bool s_ug = false;

    std::string dir( directory );

    FileSystem::dirAddSlashAtEnd( dir );

    // This should never happen here
    if( !FileSystem::isDirectory( dir ) )
    {
        return Errors::Log::createLastError( Errors::FileNotFound, dir );
    }
    else if ( pathInWatches( dir ) || pathInParent( dir ) )
    {
        return Errors::Log::createLastError( Errors::FileRepeated, directory );
    }
    else if ( NULL != parent && FileSystem::isRemoteFS( dir ) )
    {
        return Errors::Log::createLastError( Errors::FileRemote, dir );
    }

    std::string curPath;
    std::string link( FileSystem::getLinkRealPath( dir, curPath ) );

    if ( "" != link )
    {
        /// Avoid adding symlinks directories if it's now enabled
        if ( NULL != parent && !mWatcher->mFileWatcher->followSymlinks() )
        {
            return Errors::Log::createLastError( Errors::FileOutOfScope, dir );
        }

        if ( pathInWatches( link ) || pathInParent( link ) )
        {
            return Errors::Log::createLastError( Errors::FileRepeated, link );
        }
        else if ( !mWatcher->linkAllowed( curPath, link ) )
        {
            return Errors::Log::createLastError( Errors::FileOutOfScope, link );
        }
        else
        {
            dir = link;
        }
    }

    if ( mWatcher->availablesFD() )
    {
        WatcherKqueue* watch = new WatcherKqueue( ++mLastWatchID, dir, watcher, recursive, mWatcher, parent );

        mWatches.insert(std::make_pair(mLastWatchID, watch));

        watch->addAll();

        s_fc++;

        // if failed to open the directory... erase the watcher
        if ( !watch->initOK() )
        {
            int le = watch->lastErrno();

            mWatches.erase( watch->ID );

            efSAFE_DELETE( watch );

            mLastWatchID--;

            // Probably the folder has too many files, create a generic watcher
            if ( EACCES != le )
            {
                WatcherGeneric * watch = new WatcherGeneric( ++mLastWatchID, dir, watcher, mWatcher, recursive );

                mWatches.insert(std::make_pair(mLastWatchID, watch));
            }
            else
            {
                return Errors::Log::createLastError( Errors::Unspecified, link );
            }
        }
    }
    else
    {
        if ( !s_ug )
        {
            efDEBUG( "Started using WatcherGeneric, reached file descriptors limit: %ld. Folders added: %ld\n", mWatcher->mFileDescriptorCount, s_fc );
            s_ug = true;
        }

        WatcherGeneric * watch = new WatcherGeneric( ++mLastWatchID, dir, watcher, mWatcher, recursive );

        mWatches.insert(std::make_pair(mLastWatchID, watch));
    }

    return mLastWatchID;
}

bool WatcherKqueue::initOK()
{
    return mInitOK;
}

void WatcherKqueue::removeWatch( WatchID watchid )
{
    WatchMap::iterator iter = mWatches.find(watchid);

    if(iter == mWatches.end())
        return;

    Watcher * watch = iter->second;

    mWatches.erase(iter);

    efSAFE_DELETE( watch );
}

bool WatcherKqueue::pathInWatches( const std::string& path )
{
    return NULL != findWatcher( path );
}

bool WatcherKqueue::pathInParent( const std::string &path )
{
    WatcherKqueue * pNext = mParent;

    while ( NULL != pNext )
    {
        if ( pNext->pathInWatches( path ) )
        {
            return true;
        }

        pNext = pNext->mParent;
    }

    if ( mWatcher->pathInWatches( path ) )
    {
        return true;
    }

    if ( path == Directory )
    {
        return true;
    }

    return false;
}

int WatcherKqueue::lastErrno()
{
    return mErrno;
}

}

#endif
